package org.hackystat.ambientdevice.trigger;

import java.util.logging.Level;
import org.hackystat.ambientdevice.ConfigurationException;
import org.hackystat.ambientdevice.server.jaxb.Project;
import org.hackystat.ambientdevice.server.jaxb.Trigger;
import org.hackystat.dailyprojectdata.client.DailyProjectDataClient;
import org.hackystat.dailyprojectdata.client.DailyProjectDataClientException;
import org.hackystat.dailyprojectdata.resource.coverage.jaxb.ConstructData;
import org.hackystat.dailyprojectdata.resource.coverage.jaxb.CoverageDailyProjectData;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Trigger that detect coverage decrease.
 * Activate when coverage has dropped by a given threshold.
 * @author Shaoxuan Zhang
 *
 */
public class CoverageTrigger extends HackystatTrigger {

  protected static final double DEVIATION = 0.0001;
  protected DailyProjectDataClient client;
  protected double currentCoverage = 0;
  private String property;
  protected static final String[] validProperties = { "DropBy", "Compare" };

  /**
   * Construct this instance with given parameters.
   * 
   * @param project the project to monitor
   * @param trigger the jaxb trigger
   * @param client the DailyProjectData Client
   * @throws ConfigurationException if the property is not supported
   */
  @SuppressWarnings("PMD.ConfusingTernary")
  public CoverageTrigger(Project project, Trigger trigger, DailyProjectDataClient client)
    throws ConfigurationException {
    super(project, trigger);
    this.client = client;
    this.property = trigger.getProperty();
    if (trigger.getId() == null) {
      this.setId("Coverage " + this.property);
      if (this.getValue() != null) {
        this.setId(this.getId() + "(" + Double.valueOf(this.getValue()) * 100 + "%)");
      }
      else if (this.getMinValue() != null || this.getMaxValue() != null) {
        if (this.getMinValue() == null) {
          this.setMinValue(0.0);
        }
        if (this.getMaxValue() == null) {
          this.setMaxValue(1.0);
        }
        if (this.getMinValue() > 1.0 || this.getMaxValue() > 1.0) {
          throw new ConfigurationException(this.getId() + ": minValue or maxValue is invalid: "
              + this.getMinValue() + "-" + this.getMaxValue() + " (should be 0.0~1.0)");
        }
        this.setId(this.getId() + "(" + this.getMinValue() * 100 + "% - " + this.getMaxValue()
            * 100 + "%)");
      }
      else {
        throw new ConfigurationException(this.getId() + ": no valid value is found.");
      }
    }
    else {
      this.setId("Coverage " + trigger.getId());
    }
    boolean valid = false;
    for (String property : validProperties) {
      if (property.equals(this.property)) {
        valid = true;
        break;
      }
    }
    if (!valid) {
      throw new ConfigurationException(this.getId() + ": Property " + property
          + " is not supported");
    }
  }

  /**
   * Check the project's coverage state. 
   * Return true if the coverage has dropped by more than the threshold.
   * Retrieve data from Hackystat DailyProjectData service,
   * begin from the current. If no valid data found, then lookback day by day until find valid data
   * or reach the MAX_LOOKBACK_COUNT.
   * @return true if the coverage has dropped by more than the threshold
   */
  public boolean getState() {
    Double coverageValue;
    try {
      coverageValue = this.getCoverageData();
    }
    catch (DailyProjectDataClientException e) {
      logger.warning(this.getId() + ": error occur when getting data from DPD server.");
      if (logger.getLevel() == Level.FINEST) {
        e.printStackTrace();
      }
      return false;
    }
    if (coverageValue.isNaN()) {
      logger.fine(this.getId() + ": no new coverage data is found.");
      coverageValue = this.currentCoverage;
    }
    else {
      logger.fine(this.getId() + ": new coverage value is " + coverageValue * 100 + "%");
    }
    if ("DropBy".equalsIgnoreCase(this.property)) {
      return this.compareDropByCoverage(coverageValue);
    }
    if ("Compare".equalsIgnoreCase(this.property)) {
      return this.compareCoverage(coverageValue);
    }
    return false;
  }

  /**
   * 
   * @param newCoverage the new Coverage
   * @return whether it works or not
   */
  private boolean compareDropByCoverage(double newCoverage) {
    double difference = this.currentCoverage - newCoverage;
    if (difference < DEVIATION && difference > -DEVIATION) {
      //ignore the change that within the deviation.
      logger.fine(this.getId() + ": no coverage change detected.");
      return false;
    }
    boolean currentState;
    double change = Double.valueOf(this.getValue());
    logger.fine(this.getId() + ": coverage changed by " + difference * 100 + "%");
    if (difference >= change) {
      logger.fine(this.getId() + ": active.");
      currentState = true;
    }
    else {
      currentState = false;
    }
    this.currentCoverage = newCoverage;
    logger.fine(this.getId() + ": inactive.");
    return currentState;
  }
  
  
  /**
   * 
   * @param newCoverage the new coverage
   * @return whether it works or not
   */
  private boolean compareCoverage(double newCoverage) {
    if (this.getMaxValue() != null && newCoverage > this.getMaxValue()) {
      return false;
    }
    if (this.getMinValue() != null && newCoverage < this.getMinValue()) {
      return false;
    }
    return true;
  }
  
  
  /**
   * 
   * @return the data on the coverage
   * @throws DailyProjectDataClientException for some error
   */
  private double getCoverageData() throws DailyProjectDataClientException {
    setLastUpdateTime(Tstamp.makeTimestamp());
    CoverageDailyProjectData coverage = client.getCoverage(getProject().getOwner(), getProject()
        .getProjectName(), Tstamp.incrementDays(getLastUpdateTime(), -1), "line");
    logger.finest(this.getId() + ": getting data of project " + getProject().getOwner() + "-"
        + getProject().getProjectName());
    int numCovered = 0;
    int numUncovered = 0;
    for (ConstructData data : coverage.getConstructData()) {
      numCovered += data.getNumCovered();
      numUncovered += data.getNumUncovered();
    }
    logger.finest(this.getId() + ": numCovered = " + numCovered);
    logger.finest(this.getId() + ": numUncovered = " + numUncovered);
    return (double) numCovered / (numCovered + numUncovered);
  }

  /**
   * @param property the property to set
   */
  public void setProperty(String property) {
    this.property = property;
  }

  /**
   * @return the property
   */
  public String getProperty() {
    return property;
  }
}
